<!doctype html>
<html>
    <head>
        <title>sitelen esun: sitelen pi nasin mani</title>
        <meta charset="utf-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <meta name="description" content="nasin mani la, jan li ken esun e ijo ale... o esun kin e nimi pi toki pona!">
        <meta name="theme-color" content="#FFFFF">
        <link rel="stylesheet" href="style.css" />
    </head>
    <body>
        <div id="poki">
            <h1 class="brand">sitelen esun<sup>tm</sup> <small><a href="about.html">[seme?]</a> <a href="#" onclick="toggleOptions()">[options]</a></small></h1>
            <div id="options" style="display: none">
                <label>Image Scale (multiplier)<br>
                    <input type="number" name="scale" step="0.01" min="0.01">
                </label>
                <label>Text spacing (px)<br>
                    <input type="number" name="textSpacing" step="1" min="0">
                </label>
                <label>Line spacing (px)<br>
                    <input type="number" name="lineSpacing" step="1" min="0">
                </label>
                <label>Canvas padding (px)<br>
                    <input type="number" name="padding" step="1" min="0">
                </label>
                <label>Canvas width (px)<br>
                    <input type="number" name="width" step="1" min="0">
                </label>
                <button onclick="resetOptions()">Reset</button>
            </div>
            <h3 id="loading" style="text-align: center; font-size: x-large; font-family: You2013_Regular;">Loading...</h3>
            <canvas id="lipu-pi-sitelen-esun" width="1" height="1"></canvas>
            <textarea rows="5" id="toki">toki! mi jan Nikola, li kama pana e nasin sitelen sin tawa sina... ni li sitelen esun a!
nasin mani li lon ma ale la, jan li ken esun e ijo ale... sitelen esun la, sina ken esun kin e nimi pi toki pona!</textarea>
            <script>
                let canvas, ctx;
                let imgs, max_height, symbols;

                let options;
                let optionsInputs = document.querySelectorAll('#options input');
                let refresh;


                function toggleOptions() {
                    let block = document.querySelector('#options');
                    block.style.display = block.style.display == 'none' ? 'flex' : 'none';
                }
                function saveOptions() {
                    optionsInputs.forEach((el) => {
                        options[el.name] = +el.value;
                    });

                    window.localStorage.setItem('options', JSON.stringify(options));
                }
                function restoreOptions() {
                    options = JSON.parse(
                        window.localStorage.getItem('options') ||
                        // Defaults
                        "{\"scale\":1,\"padding\":15,\"textSpacing\":3,\"lineSpacing\":10,\"width\":0}"
                    );

                    // Defalut width for the canvas
                    if(options.width == 0) {
                        let poki = document.querySelector('#poki');
                        let pokiStyle = window.getComputedStyle(poki);

                        options.width = window.innerWidth - pokiStyle.marginLeft.replace('px', '') - pokiStyle.marginRight.replace('px', '');
                    }

                    optionsInputs.forEach((el) => {
                        el.value = options[el.name];
                    });
                }
                function resetOptions() {
                    localStorage.removeItem('options');
                    restoreOptions();
                    refresh();
                }

                document.addEventListener('DOMContentLoaded', function() {
                    restoreOptions();

                    optionsInputs.forEach((el) => {
                        el.addEventListener('change', function() {
                            saveOptions();
                            refresh();
                        });
                    });

                    document.body.onresize = function() {
                        restoreOptions(); // Auto-resize the canvas if no width is defined
                        refresh();
                        console.log(11);
                    };

                    canvas = document.querySelector('#lipu-pi-sitelen-esun');

                    symbols = [
                        '.', '!', '"', ',', ':', ';', '?', 'a', 'akesi', 'ala', 'alasa', 'ale', 'ali', 'anpa', 'ante', 'anu', 'awen', 'e', 'en', 'epiku', 'esun', 'ijo', 'ike', 'ilo', 'insa', 'jaki', 'jan', 'jasima', 'jelo', 'jo', 'kala', 'kalama', 'kama', 'kasi', 'ken', 'kepeken', 'kijetesantakalu', 'kili', 'kin', 'kipisi', 'kiwen', 'ko', 'kokosila', 'kon', 'kule', 'kulupu', 'kute', 'ku', 'la', 'lanpan', 'lape', 'laso', 'lawa', 'leko', 'len', 'lete', 'li', 'lili', 'linja', 'lipu', 'loje', 'lon', 'luka', 'lukin', 'lupa', 'ma', 'mama', 'mani', 'meli', 'meso', 'mi', 'mije', 'misikeke', 'moku', 'moli', 'monsi', 'monsuta', 'mu', 'mun', 'musi', 'mute', 'n', 'namako', 'nanpa', 'nasa', 'nasin', 'nena', 'ni', 'nimi', 'noka', 'o', 'oko', 'olin', 'ona', 'open', 'pakala', 'pali', 'palisa', 'pan', 'pana', 'pi', 'pilin', 'pimeja', 'pini', 'pipi', 'poka', 'poki', 'pona', 'pu', 'sama', 'seli', 'selo', 'seme', 'sewi', 'sijelo', 'sike', 'sin', 'sina', 'sinpin', 'sitelen', 'soko', 'sona', 'soweli', 'suli', 'suno', 'supa', 'suwi', 'tan', 'taso', 'tawa', 'telo', 'tenpo', 'toki', 'tomo', 'tonsi', 'tu', 'unpa', 'uta', 'utala', 'walo', 'wan', 'waso', 'wawa', 'weka', 'wile'
                    ];

                    let transparentImages = ['coke', 'starbucks', 'starbucks-over', 'n'];

                    let special = ['coke', 'starbucks', 'starbucks-over'];

                    imgs = {};

                    let nb_loaded = 0;
                    max_height = 0;

                    let allImages = symbols.concat(special);

                    allImages.forEach((w) => {
                        let img = new Image();
                        let ext = transparentImages.indexOf(w) == -1 ? '.jpg' : '.png';
                        img.src = 'img/' + encodeURIComponent(w).replaceAll("%", "") + ext;
                        img.onload = (x) => {
                            nb_loaded++;
                            max_height = Math.max(max_height, img.height);
                            if(nb_loaded == allImages.length)
                                ready();
                        }
                        img.onerror = function(x) {
                            console.log("Can't load", w);
                        }
                        imgs[w] = img;
                    });

                    ctx = canvas.getContext("2d");
                });

                function drawImage(word, x, y, scale) {
                    scale = scale || 1;

                    let img = imgs[word];
                    let ratio = img.height / img.width;

                    let height = img.height * scale;
                    let width = img.width * scale;

                    ctx.drawImage(img, x, y - height / 2, width, height);
                }

                function normalizedText(text) {
                    let replaced = text.replace(/[”“]/g, '"')
                                       .replace(/…/g, "...");

                    let old;
                    // Repeated n (nnnnnnnnn...)
                    while(replaced != old) {
                        old = replaced;
                        replaced = replaced.replace(/\bn(n+)\b/g, 'n\u2009$1');
                    }

                    return replaced;
                }

                let textarea = document.querySelector('#toki');

                refresh = function() {
                    let normalized = normalizedText(textarea.value);
                    drawAllText(normalized);
                }

                textarea.addEventListener('input', refresh);

                function ready() {
                    document.querySelector('#loading').style.display = 'none';
                    refresh();
                }


                // https://stackoverflow.com/a/6969486
                function escapeRegExp(string) {
                    return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string
                }

                function drawAllText(text) {
                    let scale = 0.065 * options.scale;
                    let colHeight = max_height * scale + options.lineSpacing;
                    let padding = options.padding;
                    let textSpacing = options.textSpacing;
                    let next = {x: padding, y: padding + colHeight / 2};

                    let nextStack = [];

                    function saveCtx() {
                        let clone = {x: next.x, y: next.y};
                        nextStack.push(clone);
                    }
                    function restoreCtx() {
                        next = nextStack.pop();
                    }

                    let nbLines = 1;

                    function tokenWidth(token) {
                        if(token.type == 'word') {
                            let word = token.value;
                            return imgs[word].width * scale + textSpacing;
                        } else if(token.type == 'foreign' || token.type == 'nimisin') {
                            let xOffset = token.type == 'foreign' ? 0.85 : 0.68;
                            let imgName = token.type == 'foreign' ? 'coke' : 'starbucks';

                            let groups = groupSyllables(token.value);

                            let width = imgs[imgName].width * scale;
                            let lastOffset = width * xOffset * (groups.length-1);

                            return width + textSpacing + lastOffset;
                        } else {
                            return 0;
                        }
                    }
                    function wordFitsOnLine(token) {
                        return next.x + tokenWidth(token) < options.width - padding;
                    }

                    function newline() {
                        next.x = padding;
                        next.y += colHeight;
                        nbLines++;
                    }

                    function forwardToken(token) {
                        next.x += tokenWidth(token);
                    }

                    function space() {
                        next.x += 2 * textSpacing;
                    }

                    function thinSpace() {
                        next.x += -5 * textSpacing;
                    }

                    // Words are drawn after the correct number of lines has been computed
                    let doItLater = [];

                    function drawWord(token, nextToken) {
                        let word = token.value;
                        if(!wordFitsOnLine(token)) {
                            newline();
                        } else if(nextToken.isPunctuation) {
                            saveCtx();
                            forwardToken(token);
                            let shouldReturn = !wordFitsOnLine(nextToken);
                            restoreCtx();

                            if(shouldReturn)
                                newline();
                        }

                        let x = next.x, y = next.y;
                        if(token.type == 'word') {
                            doItLater.push(() => {
                                drawImage(word, x, y, scale);
                            });
                        } else if(token.type == 'foreign') {
                            doItLater.push(() => {
                                drawForeign(word, x, y);
                            });
                        } else {
                            doItLater.push(() =>{
                                drawNimisin(word, x, y);
                            });
                        }

                        forwardToken(token);
                    }

                    function splitSyllables(word) {
                        let syllables = [];

                        let syllableRegex = "(Jan|Jen|Jon|Jun|Kan|Ken|Kin|Kon|Kun|Lan|Len|Lin|Lon|Lun|Man|Men|Min|Mon|Mun|Nan|Nen|Nin|Non|Nun|Pan|Pen|Pin|Pon|Pun|San|Sen|Sin|Son|Sun|Tan|Ten|Ton|Tun|Wan|Wen|Win|An|En|In|On|Un|Ja|Je|Jo|Ju|Ka|Ke|Ki|Ko|Ku|La|Le|Li|Lo|Lu|Ma|Me|Mi|Mo|Mu|Na|Ne|Ni|No|Nu|Pa|Pe|Pi|Po|Pu|Sa|Se|Si|So|Su|Ta|Te|To|Tu|Wa|We|Wi|A|E|I|O|U|jan|jen|jon|jun|kan|ken|kin|kon|kun|lan|len|lin|lon|lun|man|men|min|mon|mun|nan|nen|nin|non|nun|pan|pen|pin|pon|pun|san|sen|sin|son|sun|tan|ten|ton|tun|wan|wen|win|ja|je|jo|ju|ka|ke|ki|ko|ku|la|le|li|lo|lu|ma|me|mi|mo|mu|na|ne|ni|no|nu|pa|pe|pi|po|pu|sa|se|si|so|su|ta|te|to|tu|wa|we|wi|..|.)";

                        while(word.length) {
                            let match = word.match(syllableRegex)[0];
                            syllables.push(match)
                            word = word.slice(match.length);
                        }

                        return syllables;
                    }

                    function groupsOfN(arr, n) {
                        let out = [];

                        for(let i=0; i<arr.length; i++) {
                            if(i % n == 0)
                                out.push([]);
                            out[out.length-1].push(arr[i]);
                        }

                        return out;
                    }

                    function groupSyllables(word) {
                        let syllables = splitSyllables(word);
                        let out = [];

                        let first = syllables.slice(0, 3);
                        let rest = syllables.slice(3);

                        return [first].concat(groupsOfN(rest, 2)).map((group) => group.join(''));
                    }

                    function drawCustom(word, x, y, xOffset, yOffset, baseImg, lastImg, baseFontSize, fontName, fontColor) {
                        let groups = groupSyllables(word);

                        let width = imgs[baseImg].width * scale, height = imgs[baseImg].height * scale;

                        let nbImages = groups.length;
                        for(let i=nbImages-1; i>=0; i--) {
                            let offset = width * xOffset * i;
                            drawImage((i == nbImages-1 ? lastImg : baseImg), x + offset, y, scale);
                        }

                        let fontSize = baseFontSize * options.scale;

                        ctx.textAlign = "center";
                        ctx.font = fontSize + "px " + fontName;
                        ctx.fillStyle = fontColor;

                        for(let i=0; i<nbImages; i++) {
                            let offset = width * xOffset * i;
                            ctx.fillText(groups[i], x + width / 2 + offset,  y - height * yOffset);
                        }
                    }

                    function drawForeign(word, x, y) {
                        drawCustom(word, x, y, 0.85, 0, 'coke', 'coke', 34, 'You2013_Regular', 'white');
                    }

                    function drawNimisin(word, x, y) {
                        drawCustom(word, x, y, 0.68, 0.1, 'starbucks-over', 'starbucks', 22, 'pecita', '#442222');
                        return;


                        let width = imgs['starbucks'].width * scale, height = imgs['starbucks'].height * scale;
                        let fontSize = 34 * options.scale;
                        drawImage('starbucks', x, y, scale);
                        ctx.textAlign = "center";
                        ctx.font = fontSize + "px pecita";
                        ctx.fillStyle = "black";
                        ctx.fillText(word, x + width / 2, y - height * 0.1);
                    }

                    function Token(value, type) {
                        this.value = value;
                        this.type = type;
                        this.isPunctuation = !!value.match(/^[.!",:;?]$/);
                    }
                    let parsedTokens = [];

                    // Extraire un mot à la fois
                    while(text.length) {
                        let found = false;

                        // Match known symbol
                        for(let i=0; i<symbols.length; i++) {

                            let regex = '^(' + escapeRegExp(symbols[i]) + ')';
                            if(symbols[i].match(/[a-z]/))
                                regex += '\\b';

                            let match = text.match(new RegExp(regex));
                            if(match) {
                                let word = match[0];
                                text = text.slice(word.length);
                                parsedTokens.push(new Token(word, 'word'));
                                found = true;
                            }
                        }

                        if(found) continue;

                        // Match space
                        if(text[0] == ' ') {
                            parsedTokens.push(new Token(' ', 'space'));
                            text = text.slice(1);
                            continue;
                        }

                        // Match newline
                        if(text[0] == '\n') {
                            parsedTokens.push(new Token('\n', 'newline'));
                            text = text.slice(1);
                            continue;
                        }

                        if(text[0] == '\u2009') {
                            parsedTokens.push(new Token('\u2009', 'thinSpace'));
                            text = text.slice(1);
                            continue;
                        }

                        // Else: foreign word or nimisin
                        let word = text.match(/^[^ \n.!",:;?]+/)[0];
                        text = text.slice(word.length);

                        if(word[0].toUpperCase() == word[0]) {
                            //throw word + ' is a foreign word and is not suppported';
                            parsedTokens.push(new Token(word, 'foreign'));
                        } else {
                            //throw word + ' is a nimisin and is not suppported';
                            parsedTokens.push(new Token(word, 'nimisin'));
                        }
                    }

                    let fcts = {
                        word: drawWord,
                        space: space,
                        newline: newline,
                        thinSpace: thinSpace,
                        foreign: drawWord,
                        nimisin: drawWord,
                    };

                    for(let i=0; i<parsedTokens.length; i++) {
                        let token = parsedTokens[i];
                        let nextToken;
                        if(i < parsedTokens.length - 1)
                            nextToken = parsedTokens[i + 1];
                        else
                            nextToken = new Token('', 'eof');

                        fcts[token.type](token, nextToken);
                    }

                    canvas.height = nbLines * colHeight + padding * 2;
                    canvas.width = options.width;

                    ctx.fillStyle = 'white';
                    ctx.fillRect(0, 0, canvas.width, canvas.height);
                    doItLater.forEach((f) => f());
                }
            </script>
        </div>
    </body>
</html>
